package com.katesoft.scale4j.web.webconsole;

import static com.katesoft.scale4j.common.io.FileUtility.LINE_SEPARATOR;
import static com.katesoft.scale4j.common.utils.StringUtility.isEmpty;
import static com.katesoft.scale4j.persistent.utils.Perf4jLoggerUtility.findGraphingStatisticsAppender;
import it.openutils.log4j.Log4jConfigurationServlet;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import org.mortbay.jetty.Handler;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.DefaultHandler;
import org.mortbay.jetty.handler.HandlerCollection;
import org.mortbay.jetty.security.Constraint;
import org.mortbay.jetty.security.ConstraintMapping;
import org.mortbay.jetty.security.SecurityHandler;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.ServletHolder;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.thread.QueuedThreadPool;
import org.perf4j.log4j.GraphingStatisticsAppender;
import org.perf4j.log4j.servlet.GraphingServlet;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;

import com.katesoft.scale4j.common.utils.StringUtility;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;

/**
 * This is jetty server with some additional functionality:
 * <p/>
 * 1)log4j configuration console available by default on /log4j context path.
 * <p/>
 * 2)perf4j statistic viewer available by default on /perf4j context path.
 * <p/>
 * 3)hazelcast monitor GWT web application available by default on /hazelcast context path
 * <p/>
 * Client can use this server as standard jetty server - additional functionality of this class will
 * not override user's handler, but will add functionality. You can customize context path for
 * log4j, perf4j, and hazelcast by using setters for those properties.
 * 
 * @author kate2007
 */
public class WebMonitor extends Server implements InitializingBean {
   private final Logger logger = LogFactory.getLogger(getClass());
   //
   private String contextPath = "/";
   private String perf4jPath = "/perf4j";
   private String log4jPath = "/log4j";
   private String hazelcastPath = "/hazelcast";
   //
   private String graphNames;
   private String hazelcastWarLocation;
   private AuthenticationManager authenticationManager;

   public WebMonitor() {
      setThreadPool(new QueuedThreadPool(1000));
   }

   /**
    * allows to set context path of web monitoring console.
    * 
    * @param contextPath
    *           path
    */
   public void setContextPath(String contextPath) {
      if (!isEmpty(contextPath)) {
         this.contextPath = contextPath;
      }
   }

   /**
    * allows to set context path for perf4j servlet.
    * 
    * @param perf4jPath
    *           path
    */
   public void setPerf4jPath(String perf4jPath) {
      if (!isEmpty(perf4jPath)) {
         this.perf4jPath = perf4jPath;
      }
   }

   /**
    * allows to set context path for hazelcast servlet.
    * 
    * @param hazelcastPath
    *           path
    */
   public void setHazelcastPath(String hazelcastPath) {
      if (!isEmpty(hazelcastPath)) {
         this.hazelcastPath = hazelcastPath;
      }
   }

   /**
    * allows to set context path for log4j configuration servlet.
    * 
    * @param log4jPath
    *           path
    */
   public void setLog4jPath(String log4jPath) {
      if (!isEmpty(log4jPath)) {
         this.log4jPath = log4jPath;
      }
   }

   /** @return context path for perf4j servlet(default is /perf4j). */
   public String getPerf4jPath() {
      return perf4jPath;
   }

   /** @return context path of log4f servlet(default is /log4j). */
   public String getLog4jPath() {
      return log4jPath;
   }

   /** @return context path of hazelcast monitor(default is /hazelcast). */
   public String getHazelcastPath() {
      return hazelcastPath;
   }

   /**
    * The graphNames parameter determines which graphs to expose.
    * <p/>
    * The param-value should be a comma-separated list of the appender NAMES as defined in the
    * log4j.xml file.
    * <p/>
    * NOTE: you can leave this property as null and do not inject list - in this case graphNames
    * will be auto-detected.
    * 
    * @param graphNames
    *           comma-separated list of the appender names.
    */
   public void setGraphNames(String graphNames) {
      this.graphNames = graphNames;
   }

   /**
    * you can manually specify location of hazelcast-monitor.war
    * 
    * @param hazelcastWarLocation
    *           actual location of war
    */
   public void setHazelcastWarLocation(String hazelcastWarLocation) {
      this.hazelcastWarLocation = hazelcastWarLocation;
   }

   /**
    * create web application for hazelcast web-monitor.
    * <p/>
    * Take a look here <a href="http://hazelcast.com/documentation.jsp#Monitoring">monitor</a>
    * 
    * @return webAppContext for hazelcast war file
    */
   protected WebAppContext setupHazelcast() {
      if (hazelcastWarLocation != null) {
         WebAppContext webapp = new WebAppContext();
         webapp.setContextPath(hazelcastPath);
         webapp.setWar(hazelcastWarLocation);
         return webapp;
      }
      return null;
   }

   /**
    * set-up log4j configuration servlet
    * 
    * @param context
    *           servlet context(log4j servlet should be registered with-in context).
    */
   protected void setupLog4jConfigurationServlet(Context context) {
      ServletHolder holder = new ServletHolder();
      Log4jConfigurationServlet servlet = new Log4jConfigurationServlet();
      holder.setServlet(servlet);
      holder.setName("log4j");
      logger.info("registering log4j servlet holder[%s]", getLog4jPath());
      context.addServlet(holder, getLog4jPath());
   }

   /**
    * set-up perf4j configuration servlet.
    * 
    * @param context
    *           servlet context(perf4j servlet should be registered with-in context).
    */
   protected void setupPerf4jServlet(Context context) {
      Map<String, String> initParameters = Collections.singletonMap("graphNames", graphNames);
      ServletHolder holder = new ServletHolder();
      GraphingServlet servlet = new GraphingServlet();
      holder.setServlet(servlet);
      holder.setName("perf4j");
      holder.setInitParameters(initParameters);
      logger.info("registering perf4j servlet holder[%s]", getPerf4jPath());
      context.addServlet(holder, getPerf4jPath());
   }

   /**
    * allows to specify authenticationManager to be used to restrict access to webconsole.
    * 
    * @param authenticationManager
    *           manager
    */
   public void setAuthenticationManager(AuthenticationManager authenticationManager) {
      this.authenticationManager = authenticationManager;
   }

   /**
    * @return true if hazelcast-monitor web application available in classpath or if user specified
    *         location of war manually.
    */
   public boolean isHazelcastAvailable() {
      return !isEmpty(hazelcastWarLocation);
   }

   protected SecurityHandler getSecurityRestrictionHandler() {
      Constraint constraint = new Constraint();
      constraint.setName(Constraint.__BASIC_AUTH);
      constraint.setAuthenticate(true);
      constraint.setRoles(new String[] { Constraint.ANY_ROLE });
      constraint.setAuthenticate(true);
      ConstraintMapping cm = new ConstraintMapping();
      cm.setConstraint(constraint);
      cm.setPathSpec("/*");
      SecurityHandler sh = new SecurityHandler();
      sh.setConstraintMappings(new ConstraintMapping[] { cm });
      sh.setUserRealm(new JettySpringSecurityUserRealm(authenticationManager));
      sh.setCheckWelcomeFiles(false);
      return sh;
   }

   @Override
   public void afterPropertiesSet() throws Exception {
      if (StringUtility.isEmpty(graphNames)) {
         Collection<GraphingStatisticsAppender> statisticsAppender = findGraphingStatisticsAppender();
         StringBuilder builder = new StringBuilder();
         for (Iterator<GraphingStatisticsAppender> iterator = statisticsAppender.iterator(); iterator
                  .hasNext();) {
            GraphingStatisticsAppender appender = iterator.next();
            builder.append(appender.getName());
            if (iterator.hasNext()) {
               builder.append(",");
            }
            graphNames = builder.toString();
         }
      }
      logger.info("runtime performance graphNames = [%s]", graphNames);
      //
      if (StringUtility.isEmpty(hazelcastWarLocation)) {
         Properties properties = new Properties();
         properties.load(new ClassPathResource("hazelcast-runtime.properties").getInputStream());
         String hazelcast = String.format("hazelcast-monitor-%s.war",
                  properties.getProperty("hazelcast.version"));
         String classpath = System.getProperty("java.class.path");
         logger.info("looking %s in classpath%s[%s]", hazelcast, LINE_SEPARATOR, classpath);
         String[] arr = classpath.split(";");
         for (String s : arr) {
            if (s.contains(hazelcast)) {
               hazelcastWarLocation = s;
               break;
            }
         }
      }
      if (hazelcastWarLocation != null) {
         logger.info("using hazelcast-monitor web archive on location=%s", hazelcastWarLocation);
      } else {
         logger.warn("unable to find hazelcast-monitor in classpath!");
      }
      //
      Context servletContext = new Context();
      WebAppContext webAppContext = setupHazelcast();
      servletContext.setContextPath(contextPath);
      setupPerf4jServlet(servletContext);
      setupLog4jConfigurationServlet(servletContext);
      //
      HandlerCollection handlerCollection = new HandlerCollection();
      Handler userHandler = getHandler();
      if (userHandler != null) {
         handlerCollection.addHandler(userHandler);
      }
      if (webAppContext != null) {
         handlerCollection.addHandler(webAppContext);
      }
      handlerCollection.addHandler(servletContext);
      if (authenticationManager != null) {
         SecurityHandler securityHandler = getSecurityRestrictionHandler();
         if (webAppContext != null) {
            webAppContext.addHandler(securityHandler);
         }
      }
      if (userHandler == null) {
         handlerCollection.addHandler(new DefaultHandler());
      }
      setHandler(handlerCollection);
      //
      start();
   }
}
